#include "allegro_backend.h"
#include <eastl/memory.h>

#define WIN32_LEAN_AND_MEAN

#include <allegro5/allegro5.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_ttf.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_opengl.h>
#include <gl/GL.h>

#undef DrawText
#undef LoadImage

#define __AB_LE(...) LogError(__VA_ARGS__), 0;

using namespace GTL;
using namespace GTL::Engine;

/// TODO: CRITICAL: Implement cache'ing

struct AB_Impl
{
	ALLEGRO_DISPLAY* mWindow;
	ALLEGRO_EVENT_QUEUE* mQueue;
	ALLEGRO_FONT* mMainFont;
	ALLEGRO_MOUSE_STATE* mMouseState;
};

void AllegroBackend::Init( GTL::Engine::Game* game, int width, int height, bool fullscreen )
{
	pImpl = new AB_Impl;
	mGame = game;

	al_init();
	al_init_primitives_addon();
	al_init_font_addon();
	al_init_ttf_addon();
	al_init_image_addon();
	al_install_mouse();
	al_install_keyboard();

//#ifdef GTL_DEBUG
//  al_set_new_display_flags(ALLEGRO_WINDOWED | ALLEGRO_OPENGL);
//#else
  al_set_new_display_refresh_rate(75);
  al_set_new_display_flags((fullscreen?ALLEGRO_FULLSCREEN:ALLEGRO_WINDOWED) | ALLEGRO_OPENGL);
//#endif
	pImpl->mWindow = al_create_display(width, height);
	pImpl->mQueue = al_create_event_queue();
	al_register_event_source(pImpl->mQueue, al_get_display_event_source(pImpl->mWindow));
	al_register_event_source(pImpl->mQueue, al_get_keyboard_event_source());
	al_register_event_source(pImpl->mQueue, al_get_mouse_event_source());

	SetBlendingMode(BlendMode::Alpha);

	memset(KeyboardState, 0, sizeof(KeyboardState));

	pImpl->mMouseState = new ALLEGRO_MOUSE_STATE;
	memset(pImpl->mMouseState, 0, sizeof(ALLEGRO_MOUSE_STATE));

	MouseState[0].Buttons = MouseState[1].Buttons = 0;
	MouseState[0].X = MouseState[1].X = INT32_MIN;
	MouseState[0].Y = MouseState[1].Y = INT32_MIN;
	MouseState[0].Z = MouseState[1].Z = INT32_MIN;
	MouseState[0].W = MouseState[1].W = INT32_MIN;
}

void AllegroBackend::Destroy()
{
	al_destroy_display(pImpl->mWindow);
	al_destroy_event_queue(pImpl->mQueue);

	delete pImpl->mMouseState;

	delete pImpl;
}

struct AllegroIR : public GTL::Engine::ImageRef
{
	ALLEGRO_BITMAP* bitmap;
	virtual void Draw(const Point& pos, const ColorRGBA& color) const
	{
		//al_draw_bitmap(bitmap, pos.X, pos.Y, 0);
		al_draw_tinted_scaled_bitmap(bitmap, al_map_rgba_f(color.R, color.G, color.B, color.A), 0, 0, Width, Height, pos.X, pos.Y, Width, Height, 0);
	}
	virtual void Draw(const Rect& dest, const ColorRGBA& color) const
	{
		//al_draw_bitmap_region(bitmap, 0, 0, dest.GetWidth(), dest.GetHeight(), dest.P1.X, dest.P1.Y, 0);
		al_draw_tinted_scaled_bitmap(bitmap, al_map_rgba_f(color.R, color.G, color.B, color.A), 0, 0, Width, Height, dest.P1.X, dest.P1.Y, dest.GetWidth(), dest.GetHeight(), 0);
	}
	virtual void Draw(const Rect& source, const Rect& dest, const ColorRGBA& color) const
	{
		al_draw_tinted_scaled_bitmap(bitmap, 
			al_map_rgba_f(color.R, color.G, color.B, color.A), 
			source.P1.X, source.P1.Y, source.GetWidth(), source.GetHeight(), 
			dest.P1.X, dest.P1.Y, dest.GetWidth(), dest.GetHeight(),
			0);
	}
	virtual void Draw(const Rect& source, const Point& center, const Point& dest, float rotation, const Point& scale = Point(1.0f, 1.0f), const ColorRGBA& color = ColorRGBA(1,1,1,1), int flip_flags = 0) const
	{
		ALLEGRO_BITMAP* bmp = al_create_sub_bitmap(bitmap, source.P1.X, source.P1.Y, source.GetWidth(), source.GetHeight());
		al_draw_tinted_scaled_rotated_bitmap(bmp, al_map_rgba_f(color.R, color.G, color.B, color.A), 
			center.X, center.Y, dest.X, dest.Y, scale.X, scale.Y, rotation, flip_flags);
		al_destroy_bitmap(bmp);
	}
  virtual LockStruct Lock()
  {
    ALLEGRO_LOCKED_REGION* alr = al_lock_bitmap(bitmap, al_get_bitmap_format(bitmap), ALLEGRO_LOCK_READWRITE);
    LockStruct ls;
    ls.Data = alr->data;
    ls.Format = alr->format;
    ls.Pitch = alr->pitch;
    ls.PixelSize = alr->pixel_size;
    return ls;
  }
  virtual void Unlock()
  {
    al_unlock_bitmap(bitmap);
  }
};

struct AllegroFR : public FontRef
{
	ALLEGRO_FONT* font;
	int GetByteLen(const char* str, int cplen) const
	{
		if (cplen == -1)
			return (int)strlen(str);
		else
			return GTL::Strings::UTF8::GetOffset(str, cplen);
	}
	virtual void DrawText(const Point& pos, const char* utf8str, const ColorRGBA& color, int length) const
	{
		length = GetByteLen(utf8str, length);

		ALLEGRO_USTR_INFO info;
		al_draw_ustr(font, al_map_rgba_f(color.R, color.G, color.B, color.A), pos.X, pos.Y, 0, al_ref_buffer(&info, utf8str, length));
	}
	virtual float GetHeight() const
	{
		return (float)al_get_font_line_height(font);
	}
	virtual float GetTextWidth(const char* utf8str, int length = -1) const
	{
		length = GetByteLen(utf8str, length);

		ALLEGRO_USTR_INFO info;
		return (float)al_get_ustr_width(font, al_ref_buffer(&info, utf8str, length));
	}
};

struct AllegroSR : public ShaderRef
{
	static GLuint cached_shader_id;
	GLuint shaderID, vshaderID, pshaderID;
	AllegroSR() : shaderID(0), vshaderID(0), pshaderID(0) {}
	virtual void Use()
	{
		if (cached_shader_id == shaderID) return;
			
		glUseProgram(shaderID);
		cached_shader_id = shaderID;
	}
	virtual void Unuse()
	{
		if (cached_shader_id == 0) return;

		glUseProgram(0);
		cached_shader_id = 0;
	}
	virtual int32_t GetAttributeID(const char* str)
	{
		if (shaderID)
			return glGetAttribLocation(shaderID, str);
		else
			return -1;
	}
	virtual int32_t GetUniformID(const char* name)
	{
		return glGetUniformLocation(shaderID, name);
	}
	virtual void SetUniform(int32_t uniformID, float val)
	{
		glUniform1f(uniformID, val);
	}
	virtual void SetUniform(int32_t uniformID, const Point& val)
	{
		glUniform2f(uniformID, val.X, val.Y);
	}
};

GLuint AllegroSR::cached_shader_id = 0;

bool CompileShader(AllegroBackend* back, const char* filename, GLuint shader)
{
	char* vsrc = GTL::Files::Just::LoadEntireTextFile(filename);
	if (!vsrc) return back->__AB_LE("Could not load shader file");
	glShaderSource(shader, 1, (const GLchar**)&vsrc, NULL);
	delete[] vsrc;

	glCompileShader(shader);
	int ret = GL_FALSE;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &ret);
	if (ret == GL_FALSE)
	{
		// Report error
		glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &ret);
		eastl::vector<char> log(ret);
		int retsize;
		glGetShaderInfoLog(shader, ret, &retsize, &log[0]);
		back->LogError("-- Shader Compile Error: --");
		back->LogError(&log[0]);
		back->LogError("\n-----------------------");

		return back->__AB_LE("Could not compile shader file");
	}

	return true;
}

ShaderRef* AllegroBackend::LoadShader(const char* vshader, const char* pshader)
{
	AllegroSR* sr = new AllegroSR;
	GTL::Utilities::ScopeDeferDelete<AllegroSR> deleter(sr);

	LogResource("Creating shader");

	sr->shaderID = glCreateProgram();
	if (!sr->shaderID) return __AB_LE("Could not create shader program");

	sr->vshaderID = glCreateShader(GL_VERTEX_SHADER);
	if (!sr->vshaderID) return __AB_LE("Could not create vertex shader");

	sr->pshaderID = glCreateShader(GL_FRAGMENT_SHADER);
	if (pshader && !sr->pshaderID) return __AB_LE("Could not create pixel shader");
	
	if (!CompileShader(this, vshader, sr->vshaderID)) return __AB_LE("Could not compile vertex shader");
	glAttachShader(sr->shaderID,sr->vshaderID);

	if (pshader)
	{
		if (!CompileShader(this, pshader, sr->pshaderID)) return __AB_LE("Could not compile pixel shader");
		glAttachShader(sr->shaderID,sr->pshaderID);
	}

	glLinkProgram(sr->shaderID);
	int ret;
	glGetProgramiv(sr->shaderID, GL_LINK_STATUS, &ret);
	if (ret == GL_FALSE)
	{
		glGetProgramiv(sr->shaderID, GL_INFO_LOG_LENGTH, &ret);
		eastl::vector<char> log(ret);
		int retsize;
		glGetProgramInfoLog(sr->shaderID, ret, &retsize, &log[0]);
		LogError("-- Shader Link Error: --");
		LogError(&log[0]);
		LogError("\n-----------------------");

		return __AB_LE("Could not link shader program");
	}

	return deleter.Release();
}

ImageRef* AllegroBackend::LoadImage( const char* filename, bool is_cache_ok )
{
	ALLEGRO_BITMAP* image = al_load_bitmap(filename);
	if (!image) return __AB_LE("Could not load image `%s`", filename);

	LogResource("Creating image\n");
 
	AllegroIR* ref = new AllegroIR;
	ref->bitmap = image;
	ref->Width = (float)al_get_bitmap_width(image);
	ref->Height = (float)al_get_bitmap_height(image);
	return ref;
}

FontRef* AllegroBackend::LoadFont( const char* filename )
{
	ALLEGRO_FONT* font = al_load_font(filename, -16, ALLEGRO_TTF_MONOCHROME);
	if (!font) return __AB_LE("Could not load font `%s`", filename);

	LogResource("Creating font\n");

	AllegroFR* ref = new AllegroFR;
	ref->font = font;
	return ref;
}

void AllegroBackend::DestroyImage( GTL::Engine::ImageRef* ref )
{
	LogResource("Destroying image\n");
	AllegroIR* r = (AllegroIR*)ref;
	al_destroy_bitmap(r->bitmap);
}

void AllegroBackend::DestroyFont( GTL::Engine::FontRef* ref )
{
	LogResource("Destroying font\n");
	AllegroFR* r = (AllegroFR*)ref;
	al_destroy_font(r->font);
}

void AllegroBackend::DestroyShader( GTL::Engine::ShaderRef* ref )
{
	LogResource("Destroying shader\n");
	AllegroSR* r = (AllegroSR*)ref;
	if (r->shaderID)
		glDeleteProgram(r->shaderID);
	if (r->vshaderID)
		glDeleteShader(r->vshaderID);
	if (r->pshaderID)
		glDeleteShader(r->pshaderID);
}

uint64_t AllegroBackend::GetTimeMsec()
{
	double t = al_get_time();
	/// do shit with precision
	double a = fmod(t, 1.0);
	uint64_t tt = (uint64_t)floor(t);
	tt *= 1000;
	tt += ((uint64_t)floor(a*1000.0));
	return tt;
}

double AllegroBackend::GetTimeSec()
{
	return al_get_time();
}

class InputService : public Service
{
public:
	AllegroBackend* backend;
	InputService(AllegroBackend* back) : backend(back) {}
	virtual void Start() {}
	virtual void Shutdown() {}
	virtual int GetPriority() const { return Service::PreUpdate-100; }
	virtual int Do(float dt)
	{
		return backend->DoInput(dt);
	}

	virtual const char* GetName() const { return "Allegro Input"; }
};

class PreDrawService : public Service
{
public:
	AllegroBackend* backend;
	virtual void Start() {}
	virtual void Shutdown() {}
	virtual int GetPriority() const { return Service::Draw-100; }
	virtual int Do(float dt)
	{
		al_clear_to_color(al_map_rgb(0,0,0));
		return 0;
	}
	virtual const char* GetName() const { return "Allegro Pre-Draw"; }
};

class PostDrawService : public Service
{
public:
	AllegroBackend* backend;
	virtual void Start() {}
	virtual void Shutdown() {}
	virtual int GetPriority() const { return Service::PostDraw+100; }
	virtual int Do(float dt)
	{
		al_flip_display();

		//Sleep(10);
		GTL::Concurrency::YieldThread();

		return 0;
	}
	virtual const char* GetName() const { return "Allegro Post-Draw"; }
};

/*
/// update

Update(dt);

/// draw
al_clear_to_color(al_map_rgb(0,0,0));

Draw();

al_flip_display();

Sleep(0);*/

void AllegroBackend::RegisterServices()
{
	mGame->RegisterService(mInputService = new InputService(this));
	mGame->RegisterService(mPreDrawService = new PreDrawService());
	mGame->RegisterService(mPostDrawService = new PostDrawService());
	/// event update service, pre-draw and post-draw
}

int AllegroBackend::DoInput( float dt )
{
	al_get_mouse_state(pImpl->mMouseState);

	MouseState[1] = MouseState[0];
	MouseState[0].X = pImpl->mMouseState->x;
	MouseState[0].Y = pImpl->mMouseState->y;
	MouseState[0].Z = pImpl->mMouseState->z;
	MouseState[0].W = pImpl->mMouseState->w;
	MouseState[0].Buttons = pImpl->mMouseState->buttons;

	KeyboardState[1] = KeyboardState[0];
	
	while (!al_is_event_queue_empty(pImpl->mQueue))
	{
		ALLEGRO_EVENT ev;
		al_get_next_event(pImpl->mQueue, &ev);

		if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE)
			return 1;
		else if (ev.type == ALLEGRO_EVENT_KEY_DOWN)
		{
			KeyboardState[0].Keys[ev.keyboard.keycode] = 1;
		} else if (ev.type == ALLEGRO_EVENT_KEY_UP)
			KeyboardState[0].Keys[ev.keyboard.keycode] = 0;

		if (ev.type == ALLEGRO_EVENT_KEY_CHAR)
		{
			mGame->AddEvent(Core::Event::CharEntered, 0, GameEvent::MakeCharPair(ev.keyboard.keycode | (ev.keyboard.modifiers<<16), ev.keyboard.unichar));
		}
	}

	return 0;
}

void AllegroBackend::DebugLog( const char* str, int type )
{
	if (type == Logging::Error)
	{
		//GTL_ASSERT (0, str);
		//printf("%s\n", str);
    MessageBoxA(0, str, "Error", 0);
		GTL_CRITICAL_FAILURE(str);
	}
	else
		printf("%s\n",str);
}

void AllegroBackend::UnuseShader()
{
	if (AllegroSR::cached_shader_id == 0) return;

	glUseProgram(0);
	AllegroSR::cached_shader_id = 0;
}

GTL::Engine::ImageRef* AllegroBackend::CreateRenderTarget( size_t width, size_t height )
{
	ALLEGRO_BITMAP* bitmap = al_create_bitmap((int)width, (int)height);
	if (!bitmap) return 0;
	
	AllegroIR* ref = new AllegroIR;
	ref->bitmap = bitmap;
	ref->Width = width;
	ref->Height = height;

	return ref;
}

void AllegroBackend::SetRenderTarget( GTL::Engine::ImageRef* target )
{
	if (target)
	{
		AllegroIR* ir = (AllegroIR*)target;
		al_set_target_bitmap(ir->bitmap);
	} else
	{
		al_set_target_backbuffer(pImpl->mWindow);
	}
}

void AllegroBackend::ClearScreen( const ColorRGBA& color )
{
	al_clear_to_color(al_map_rgba_f(color.R, color.G, color.B, color.A));
}

void AllegroBackend::SetBlendingMode( int mode )
{
	switch (mode)
	{
	case BlendMode::Replace:
		al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
		break;
	case BlendMode::Additive:
		al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ONE);
		break;
	case BlendMode::Decal:
		al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
	case BlendMode::Alpha:
	default:
		al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
		break;
	}
}

void AllegroBackend::PutPixel( const Point& pos, const ColorRGBA& color )
{
  al_put_pixel(pos.X, pos.Y, al_map_rgba_f(color.R,color.G,color.B,color.A));
}

void AllegroBackend::LockDrawing( bool lock )
{
  al_hold_bitmap_drawing(lock);
}

void AllegroBackend::DrawRect( const Rect& rect, const ColorRGBA& color, const ColorRGBA& fillcolor )
{
	al_draw_filled_rectangle(rect.P1.X, rect.P1.Y, rect.P2.X, rect.P2.Y, al_map_rgba_f(fillcolor.R,fillcolor.G,fillcolor.B,fillcolor.A));
	al_draw_rectangle(rect.P1.X, rect.P1.Y, rect.P2.X, rect.P2.Y, al_map_rgba_f(color.R,color.G,color.B,color.A), 1.0f);
}

void AllegroBackend::DrawLine( float x, float y, float x2, float y2, const ColorRGBA& color )
{
	al_draw_line(x, y, x2, y2, al_map_rgba_f(color.R,color.G,color.B,color.A), 1.0f);
}

GTL::Geometry2D::GenericRectangle2D<float> AllegroBackend::GetClippingRect()
{
	int x, y, w, h;
	al_get_clipping_rectangle(&x, &y, &w, &h);
	return Rect(float(x), float(y), float(x+w), float(y+h));
}

void AllegroBackend::SetClippingRect( const GTL::Geometry2D::GenericRectangle2D<float>& rect )
{
	int x = rect.P1.X;
	int y = rect.P1.Y;
	int w = rect.GetWidth();
	int h = rect.GetHeight();
	al_set_clipping_rectangle(x, y, w, h);
}